import configparser
import subprocess
import os
import re

from common.benchmark_var import StatusCode, ConfigOptions as Options, ConfigSections as Sections
from utils.utils import Utils, ErrorHandler
from common.logger import log


class ConfigFileParser:
    """
    Configuration file parser for file in xx.ini format
    """
    # directory of config file
    _DIR = os.path.join(os.path.dirname(__file__), '../config')

    # suffix of config file
    _SUFFIX = ".ini"

    def __init__(self):
        self._config_file = None
        self._parser = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation(),
                                                 inline_comment_prefixes=['#', ','])
        self._parser.optionxform = lambda option: option
        self.options = {}
        self.option_type = {
            Options.ACTION: str,
            Options.MODE: str,
            Options.TEST_TARGET: list,
            Options.ITERATIONS: int,
            Options.COPIES: int,
            Options.TASKSET: TaskSetList,
            Options.REPORT: list,
            Options.OUTPUT_DIR: str,
            Options.SKIP_FAIL: int,
            Options.SKIP_VERIFY: int,
            Options.AUTO_UPLOAD: int,
            Options.DEBUG: int,
            Options.MANUFACTURER: str,
            Options.PRODUCT_MODEL: str,
            Options.CPU_MODEL: str,
            Options.NETCARDS: str,
            Options.RAID: str,
            Options.DISPLAY_CARDS: str,
            Options.HARDWARE_OTHERS: str,
            Options.OS: str,
            Options.SOFTWARE_OTHERS: str,
            Options.TESTER_NAME: str,
            Options.TEST_DATE: str,
            Options.BIND_COPIES: NumaList,
            Options.BIND_CORES: NumaList
        }

    def _init_options(self):
        self.options[Sections.GENERAL] = {}

    def _read_file(self, filename):
        config_dir = filename
        if not isinstance(config_dir, str):
            log.error('Invalid Filename: {}'.format(config_dir))
            return StatusCode.FILE_READ_FAIL

        if not os.path.isabs(config_dir):
            config_dir = os.path.join(self._DIR, config_dir)

        if not os.path.isfile(config_dir):
            config_dir = os.path.join(self._DIR, config_dir + self._SUFFIX)
            if not os.path.isfile(config_dir):
                log.error('File {} cannot be find'.format(config_dir))
                return StatusCode.FILE_READ_FAIL

        try:
            self._parser.read(config_dir)
        except configparser.ParsingError:
            return StatusCode.FILE_READ_FAIL

        if not self._parser.sections():
            log.error('No sections in configuration file.')
            return StatusCode.FILE_READ_FAIL

        try:
            self._parser.options(section=Sections.GENERAL)
        except configparser.ParsingError:
            log.error("No general section in configuration file")
            return StatusCode.FILE_READ_FAIL

        self._config_file = config_dir
        return StatusCode.SUCCESS

    def _parse_general(self):
        # parse general section
        for option in self._parser.options(section=Sections.GENERAL):
            origin_value = self._parser.get(Sections.GENERAL, option)
            datatype = self.option_type.get(option)
            if not datatype:
                log.error("Illegal general parameter: {}.".format(option))
                return StatusCode.INVALID_TYPE

            value = self._change_format(option, origin_value, datatype)
            self.options[Sections.GENERAL][option] = value

        return StatusCode.SUCCESS

    def _change_format(self, option, value, datatype):
        if not isinstance(value, datatype):
            try:
                if datatype == list:
                    value = list(filter(None, re.split(' |\,|\.', value)))
                value = datatype(value)
            except ValueError:
                log.error('{} has invalid type.'.format(option))
                ErrorHandler.check_error(StatusCode.INVALID_TYPE)
        return value

    def _check_compiler(self, namespace):
        is_valid = True
        if namespace == Sections.GENERAL:
            return is_valid
        options = self._parser.options(namespace)
        section = self._parser[namespace]
        if Options.CC in options and section[Options.CC].endswith("gcc"):
            if not Utils.validate_compiler_version(section[Options.CC]):
                log.error('gcc : {} version is too low, please upgrade compiler.'.format(section[Options.CC]))
                is_valid = False
        if Options.CXX in options and section[Options.CXX].endswith("g++"):
            if not Utils.validate_compiler_version(section[Options.CXX]):
                log.error('g++ : {} version is too low, please upgrade compiler.'.format(section[Options.CXX]))
                is_valid = False
        return is_valid

    def _parse_namespace(self):
        default_option = {}

        # parse compiler section
        try:
            for comp_option in self._parser.options(section=Sections.COMPILER):
                default_option[comp_option] = self._parser.get(Sections.COMPILER, comp_option)
            self.options[Sections.COMPILER] = default_option
        except configparser.NoSectionError:
            log.info('No compiler section in configuration file.')

        # parse suite section
        for namespace in self._parser.sections():
            if namespace != Sections.GENERAL and namespace != Sections.COMPILER:
                if namespace not in self.options:
                    self.options[namespace] = dict()
                for option in self._parser.options(section=namespace):
                    self.options[namespace][option] = self._parser.get(section=namespace, option=option)
            if not self._check_compiler(namespace):
                exit(1)

    def parse_ini(self, filename):
        """
        parse configuration file
        :param filename: config filename
        :return: A dict mapping options in configuration file
        """
        self._init_options()
        self._parser.clear()
        ErrorHandler.check_error(self._read_file(filename))
        ErrorHandler.check_error(self._parse_general())
        self._parse_namespace()
        return self.options


class TaskSetList(list):
    # for parse list
    _RE_SPLIT_LIST = re.compile(r'[^\w-]+')
    _RE_SEARCH_RANGE = re.compile(r'^(?P<left>\d+)(-(?P<right>\d+))?$')

    def __init__(self, value):
        if isinstance(value, str):
            lvalue = list(filter(None, value.split(',')))

        target = []
        for v in lvalue:
            pat = re.search(self._RE_SEARCH_RANGE, v)
            if pat is None:
                raise ValueError("{} is invalid.".format(value))
            left = pat.group('left')
            right = pat.group('right')
            if right is None:
                right = left
            target += list(range(int(left), int(right) + 1))
        super().__init__(target)

 
class NumaList(list):
    _RE_SEARCH_RANGE = re.compile(r'^(?P<left>\d+)(-(?P<right>\d+))?$')
    def __init__(self, value):
        if isinstance(value, str):
            lvalue = list(filter(None, value.split(',')))
        target = []
        for v in lvalue:
            pat = re.search(self._RE_SEARCH_RANGE, v)
            if pat is None:
                raise ValueError("{} is invalid.".format(value))
            left = pat.group('left')
            right = pat.group('right')
            if right is None:
                right = left
            target.append(list(range(int(left), int(right) + 1)))
        super().__init__(sorted(target, key=lambda x: x[-1]))
